/**
 * OPMERKINGEN OVER KLASSE:
 * 		checken of collisionObject isValidObject()? --> gescreven, staat vanonder, maar als het een collisionobject is gt da sowieso valid zijn 
 * 															of null nie?
 * 		bij getShips is er gecast --> gaat volgens mij moeilijk anders, of met instanceof erges iets forceren ma da gt nie beter zijn 
 * 		bij evolve omwille van recursiviteit evt geheugenproblemen aanpakken (bij grote dt)
 * 		nog na te kijken: 	Of destroy goed gaat
 * 							Of bij het setten van world in ship het hier ook wordt toegevoegd ( addAsC..O...()  )
 */

package asteroids.model;

import java.util.*;

import be.kuleuven.cs.som.annotate.*;

import ModelExceptions.IllegalShipException;
import ModelExceptions.IllegalTimeDurationException;
import ModelExceptions.IllegalValueException;
import ModelExceptions.OutOfBoundsException;
import asteroids.CollisionListener;
import asteroids.ModelException;
import asteroids.Util;

public class World {


	/**
	 * 
	 * @param height
	 * @param width
	 * @Invar |isValidSize()
	 * @invar	...
	 * 			| getLeftBoundary() < getRightBoundary()
	 * @invar 	...
	 * 			| getUpperBoundary() > getLowerBoundary()
	 * @invar	...
	 * 			|hasProperCollisionObjects()
	 */
	public World(double height, double width){
		if(!isValidWorldSize(height, width))
			throw new OutOfBoundsException("The given width and height are not applicable to create a world"); 
		this.width = width;
		this.height = height;

		gameObjects = new HashSet<CollisionObject>();
	}


	private final double maxHeight 	= 	Double.MAX_VALUE;
	private final double maxWidth 	=	Double.MAX_VALUE;
	private final double minHeight	=	0;
	private final double minWidth	=	0;
	private final double height;
	private final double width;
	private final Set<CollisionObject> gameObjects;




	/**
	 * 
	 * @param height
	 * @param width
	 * @return 	...
	 * 			|return (getHeight*getWidth)<getMaxWorldSize() && !double.isNan(getHeight()*getWidth()) && double.i
	 */
	private boolean isValidWorldSize(double height, double width){
		return (isValidHeight(height) && isValidWidth(width));
	}


	/**
	 * Get the height of the world
	 * @return The height of the world
	 */
	@Basic @Immutable
	public double getWorldHeight(){
		return this.height;
	}

	@Basic @Immutable
	private double getMinHeight(){
		return this.minHeight;
	}

	@Basic @Immutable
	private double getMaxHeight(){
		return this.maxHeight;
	}

	private boolean isValidHeight(double height){
		if(!Util.isLocatedWithinRange(getMinHeight(), getMaxHeight(), height) ||Double.isNaN(height)){
			return false;
		}
		return true;
	}	



	@Basic	@Immutable
	public double getWorldWidth(){
		return this.width;
	}

	@Basic @Immutable
	private double getMinWidth(){
		return this.minWidth;
	}

	@Basic @Immutable
	private double getMaxWidth(){
		return this.maxWidth;
	}
	/**
	 * 
	 * @param width 	The width of the world
	 * @return 			||if(!Util.isLocatedWithinRange(getMinWidth(), getMaxWidth(), width) ||Double.isNaN(width))
	 * 					||{result = true}
	 */
	private boolean isValidWidth(double width){

		if(!Util.isLocatedWithinRange(getMinWidth(), getMaxWidth(), width) ||Double.isNaN(width)){
			return false;
		}
		return true;
	}



	@Basic
	public double getLeftBoundary(){
		return 0;
	}
	@Basic
	public double getRightBoundary(){
		return getWorldWidth();
	}
	@Basic
	public double getLowerBoundary(){
		return 0;
	}
	@Basic
	public double getUpperBoundary(){
		return getWorldHeight();
	}

	/**
	 * 
	 * @param collisionObject	The Object you want to check
	 * @return					||if(collisionObject.isShip()||collisionObject.isAsteroid()||collisionObject.isBullet()){
								||return true;
								||}
	 */
	private boolean isValidObject(CollisionObject collisionObject){
		if(collisionObject.isShip()||collisionObject.isAsteroid()||collisionObject.isBullet()){
			return true;
		}
		else return false;
	}

	@Basic 
	public Set<CollisionObject> getGameObjects(){
		Set<CollisionObject> returnObjectSet = new HashSet<CollisionObject>();
		Iterator<CollisionObject> it = gameObjects.iterator();
		while(it.hasNext()){
			CollisionObject colissionObject = it.next();
			returnObjectSet.add(colissionObject);
		}
		return returnObjectSet;
	}

	@Basic 
	private int getAmountOfGameObjects(){
		return this.getGameObjects().size();
	}

	/**
	 * 
	 * @param collisionObject The object you want to check
	 * @return ||result == getGameObjects().contains(collisionObject);
	 */
	public boolean hasAsCollisionObject(CollisionObject collisionObject){
		return getGameObjects().contains(collisionObject);
	}

	/**
	 * 
	 * @param collisionObject	The collisionObject you want to add 
	 * @post					||if(canHaveAsCollisionObject(collisionObject) && collisionObject.canHaveAsWorld(this)) then
	 * 							||		hasAsCollisionObject(collisionObject) == true		
	 */
	public void addAsCollisionObject(@Raw CollisionObject collisionObject){
		if(canHaveAsCollisionObject(collisionObject) && collisionObject.isInNoWorld()){
			this.gameObjects.add(collisionObject);
			if(collisionObject.getWorld() != this)
				collisionObject.setWorld(this);
		}

	}

	//binding via wereld ook verwijderen
	// TODO modelexception?
	/**
	 * DIT NIET ALS DESTROYOBJECT OPNEMEN, SHIP ZOU EVENTUEEL LATER OOK TUSSEN WERELDEN KUNNEN REIZEN?
	 * @param collisionObject			The CollisionObject you want to remove from the world		
	 * @throws IllegalValueException	//TODO
	 * @post							||hasAsCollisionObject(collisionObject) == false
	 */
	public void removeAsCollisionObject(CollisionObject collisionObject) throws IllegalValueException{
		if(hasAsCollisionObject(collisionObject)){
			this.gameObjects.remove(collisionObject);
			if(collisionObject.getWorld()!=null)
				collisionObject.setWorld(null);
			//else already removed 
		}
		else
			throw new IllegalValueException("This object does not exists");
	}
	/**
	 * 
	 * @param collisionObject 	The CollissionObject you want to destroy					TODO WAAROM DEZE METHODE
	 * @post					||collisionObject.destroy
	 * 							||this.hasAsCollisionObject(collisionObject) == false
	 */
	public void destroyCollisionObject(CollisionObject collisionObject){
		removeAsCollisionObject(collisionObject);
		collisionObject.destroy();
	}
	// TODO tweede object meegeven om te checken of het dezelde world is?
	/**
	 * 
	 * @param collisionObject	The object you want to check
	 * @return					||if (collisionObject!=null && !collisionObject.isDestroyed())
								||result == true;
	 */

	public boolean canHaveAsCollisionObject(CollisionObject collisionObject){
		if (collisionObject!=null && !collisionObject.isDestroyed())// && collisionObject.getWorld() != this) TODO
			//if(collisionObject.isInNoWorld())
			//if (this.hasAsCollisionObject(collisionObject));
			return true;
		return false;
	}

	/**
	 * 
	 * @return	|| while(it.hasNext()){
				||CollisionObject collisionObject = it.next();
				||if(!this.canHaveAsCollisionObject(collisionObject) && collisionObject.canHaveAsWorld(this) && collisionObject.getWorld() != this)
				||return false;}
				||return true;
	}
	 */
	public boolean hasProperCollisionObjects(){

		Iterator<CollisionObject> it = gameObjects.iterator();
		while(it.hasNext()){
			CollisionObject collisionObject = it.next();
			if(collisionObject.getWorld() != this && !this.canHaveAsCollisionObject(collisionObject) && collisionObject.canHaveAsWorld(this))
				return false;
		}
		return true;
	}






	/**
	 * Return all ships located within <code>world</code>.
	 * @return	|
	 */
	public Set<Ship> getShips(){
		Set<Ship> resultSet = new HashSet<Ship>();
		Iterator<CollisionObject> it = gameObjects.iterator();
		while(it.hasNext()){
			CollisionObject collisionObject = it.next();
			try{
				if(collisionObject.isShip());
				resultSet.add((Ship) collisionObject);}
			catch (ClassCastException exc){

			}
		}
		return resultSet;

	}

	/**
	 * Return all asteroids located in <code>world</code>.
	 * @return
	 */
	public Set<Asteroid> getAsteroids(){
		Set<Asteroid> resultSet = new HashSet<Asteroid>();
		Iterator<CollisionObject> it = gameObjects.iterator();
		while(it.hasNext()){
			CollisionObject collisionObject = it.next();
			try{if(collisionObject.isAsteroid());
			resultSet.add((Asteroid)collisionObject);}
			catch(ClassCastException exc){

			}
		}
		return resultSet;
	}

	/**
	 * Return all bullets located in <code>world</code>.
	 * @return
	 */
	public Set<Bullet> getBullets(){
		Set<Bullet> resultSet = new HashSet<Bullet>();
		Iterator<CollisionObject> it = gameObjects.iterator();
		while(it.hasNext()){
			CollisionObject collisionObject = it.next();
			try{if(collisionObject.isBullet());
			resultSet.add((Bullet)collisionObject);}
			catch(ClassCastException exc){

			}
		}
		return resultSet;
	}



	/**
	 * 
	 * @return 	||"A world with the following specifications: " +
	 *			||"\n\t Height: " + getWorldHeight()+
	 *			||"\n\t Width: " +getWorldWidth()+
	 *			||"\n\t Amount of objects: " + getAmountOfGameObjects()+
	 *			||"\n\n\n\n";
	 */
	@Override
	public String toString(){
		return "A world with the following specifications: " +
				"\n\t Height: " + getWorldHeight()+
				"\n\t Width: " +getWorldWidth()+
				"\n\t Amount of objects: " + getAmountOfGameObjects()+
				"\n\n\n\n";

	}

/**
 * 
 * @return	...
 * 			|CollisionObject firstCollisionObject 
 * 			|CollisionObject firstCollisionObjectPartner
 * 			|double xFirstCollisionObject
 * 			|double yFirstCollisionObject
 * 			|for each collisionobject in this.getGameObjects():
 * 			|	for each collisionobjectpartner in this.getGameObjects():
 * 			|		double collisionTime = collisionObject.getTimeToCollision(collisionObjectPartner);
 * 			|		if(collisionTime < timeToFirstCollision && !Util.fuzzyLessThanOrEqualTo(collisionTime, 0)){
 * 			|			timeToFirstCollision = collisionTime
 * 			|			firstCollisionObject = collisionObject
 * 			|			xFirstCollisionObject = firstCollisionObject.getXCoordinate()
 * 			|			yFirstCollisionObject = firstCollisionObject.getYCoordinate()
 * 			|			firstCollisionObjectPartner = collisionObjectPartner}}}
 * 			|CollisionObject[] collisionPartners = {firstCollisionObject, firstCollisionObjectPartner};
 *			|result == collisionPartners
 */
	private CollisionObject[] getFirstCollision(){
		CollisionObject firstCollisionObject 		= null;
		CollisionObject firstCollisionObjectPartner	= null;
		double xFirstCollisionObject = 0;
		double yFirstCollisionObject = 0;
		//STEP1: FIRST COLLISION
		double timeToFirstCollision = Double.POSITIVE_INFINITY;
		for(CollisionObject collisionObject: getGameObjects()){
			for(CollisionObject collisionObjectPartner: getGameObjects()){
				if(collisionObject != null && collisionObject != collisionObjectPartner){	//impliciet getest of colisisionObjectPartner ook niet null is
					double collisionTime = collisionObject.getTimeToCollision(collisionObjectPartner);
					if(!Util.fuzzyEquals(timeToFirstCollision, Double.POSITIVE_INFINITY)&&Util.fuzzyEquals(collisionTime,timeToFirstCollision) && !Util.fuzzyEquals(xFirstCollisionObject,collisionObjectPartner.getXCoordinate())&& !Util.fuzzyEquals(yFirstCollisionObject,collisionObjectPartner.getYCoordinate())){
						collisionObjectPartner.setValidVelocity(collisionObjectPartner.getXVelocity() - 1, collisionObjectPartner.getYVelocity() - 1);
					}
					if(collisionTime < timeToFirstCollision && !Util.fuzzyLessThanOrEqualTo(collisionTime, 0)){
						//!Util.fuzzyLessThanOrEqualTo(timeToFirstCollision, collisionTime

						timeToFirstCollision = collisionTime;
						firstCollisionObject = collisionObject;
						xFirstCollisionObject = firstCollisionObject.getXCoordinate();
						yFirstCollisionObject = firstCollisionObject.getYCoordinate();

						firstCollisionObjectPartner = collisionObjectPartner;
					}
				}
			}
		}
		CollisionObject[] collisionPartners = {firstCollisionObject, firstCollisionObjectPartner};
		return collisionPartners;
	}
	
	/**
	 * 
	 * @param dt							The time you want to forward
	 * @param collisionListener				The object that is used to visually represent collisions
	 * @throws IllegalTimeDurationException	If the time is less then or equal to zero or is infinty/nan
	 * @post 	...
	 * 			||if(Util.fuzzyLessThanOrEqualTo(dt, 0)) || Double.isNaN(dt) || dt == Double.POSITIVE_INFINITY || dt== Double.NEGATIVE_INFINITY){
	 * 			||throw new IllegalTimeDurationException}
	 * @post	||if(!Util.fuzzyLessThanOrEqualTo(dt, 0)) || !Double.isNaN(dt) || !dt == Double.POSITIVE_INFINITY || !dt== Double.NEGATIVE_INFINITY){
	 *
	 * @effect	...
	 * 			| for each collisionobject in this.getGameObjects():
	 * 			|		if(!Util.fuzzyLessThanOrEqualTo(collisionobject.getTimeToCollision(),dt) then
	 * 			|			moveGameObjects(dt);
	 * @effect	...
	 * 			| 	boolean finished = false;
	 *			|	CollisionObject[] first= getFirstCollision();
	 *			|	firstCollisionObject = first[0];
	 *			|	firstCollisionObjectPartner = first[1];
	 *			|	double timeToFirstCollision = firstCollisionObject.getTimeToCollision(firstCollisionObjectPartner);
	 * 			|	while(!finished){
	 * 			|	for each collsionobject in this.getGameObjects():
	 * 			|		if(Util.fuzzyLessThanOrEqualTo(collisionTime,dt) then
	 * 			|			collisionobject.move(collisionTime);
	 * 			|			collisionobject.collisionreactionwith(collisionPartner);
	 * 			|			dt	= dt-collisionTime;
	 * 			|			
	 * 			|			finished = true;
	 * 			|			}
	 */


	public void evolve(double dt, CollisionListener collisionListener)throws IllegalTimeDurationException{
		//EIGEN KEUZE VOOR DT ONEINDIG OF 0 OF NEGATIEF OF ISNAN
		double timeToFirstCollision = Double.POSITIVE_INFINITY;
		if(Double.isNaN(dt) || dt == Double.POSITIVE_INFINITY || dt== Double.NEGATIVE_INFINITY)
			throw new IllegalTimeDurationException("Invalid timeduration dt");
		CollisionObject firstCollisionObject 		= null;
		CollisionObject firstCollisionObjectPartner	= null;
		if(!Util.fuzzyLessThanOrEqualTo(dt, 0)){			//else do nothing (if dt <=0)
			//STEP1: FIRST COLLISION
			CollisionObject[] first= getFirstCollision();
			firstCollisionObject = first[0];
			firstCollisionObjectPartner = first[1];
			if(firstCollisionObject!= null && firstCollisionObjectPartner != null )
			timeToFirstCollision = firstCollisionObject.getTimeToCollision(firstCollisionObjectPartner);
			//STEP2: ADVANCE ALL SHIPS BY TIMETOFIRSTCOLLISION OR GO TO STEP 5
			if(!Util.fuzzyEquals(timeToFirstCollision, dt) && Util.fuzzyLessThanOrEqualTo(timeToFirstCollision, dt)){//timeToFirstCollision < dt){  // TODO fuzzyequals
				for(CollisionObject collisionObject: getGameObjects()){
					collisionObject.move(timeToFirstCollision);
				}
				
				//STEP3: RESOLVE COLLISION 
				//TODO	IK DENK ZO:
				//if(firstCollisionObject != null && firstCollisionObjectPartner != null){
				//double position [] = firstCollisionObject.getCollisionPosition(firstCollisionObjectPartner);
				collisionListener.objectCollision(firstCollisionObject, firstCollisionObjectPartner,firstCollisionObject.getXCoordinate(), firstCollisionObject.getYCoordinate());
				firstCollisionObject.collisionReactionWith(firstCollisionObjectPartner);
				//collisionListener.objectCollision(firstCollisionObject, firstCollisionObjectPartner,position[0] , position[1]);

			



				//STEP4: SUBSTRACT TIMETOFIRSTCOLLSION AND GO TO STEP1
				//TODO NIET ZEKER OF JE ER ZO UITKOMT, WEL ELEGANT...
				evolve(dt-timeToFirstCollision, collisionListener);
			}
			else{//STEP 5
				moveGameObjects(dt);
			}
			dt = 0;
		}
		else{throw new IllegalTimeDurationException("That is an illegal amount of time");}
	}
	/**
	* @effect	...
	 * 			| for each collisionobject in this.getGameObjects():
	 * 			|		collisionobject.move(dt);
	 * 			|		if(collisionObject.isShip()){
	 *			|			if( ((Ship) collisionObject).isShipThrusterActive()){
	 *			|				((Ship) collisionObject).thrust(dt);}}}
	 */
	private void moveGameObjects(double dt){
		for(CollisionObject collisionObject: getGameObjects()){
			collisionObject.move(dt);
			if(collisionObject.isShip()){
				if( ((Ship) collisionObject).isShipThrusterActive()){

					((Ship) collisionObject).thrust(dt);

				}
			}
		}

	}


}





